package jmine.tec.di.annotation.visitor;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.Collections;

import jmine.tec.di.annotation.Injected;
import jmine.tec.di.cache.TypeMatchLookup;
import jmine.tec.di.metadata.DependencyMetaData;
import jmine.tec.di.metadata.impl.DependencyMetaDataImpl;
import jmine.tec.di.resolver.ObjectResolver;
import jmine.tec.di.wrapper.Wrapper;
import jmine.tec.di.wrapper.Wrappers;
import jmine.tec.utils.annotation.visitor.AnnotationVisitorAdapter;
import jmine.tec.utils.reflection.GenericTypeResolverImpl;

/**
 * @author takeshi
 */
public class InjectedAnnotationVisitor extends AnnotationVisitorAdapter<DependencyMetaData> {

    private final TypeMatchLookup<ObjectResolver> staticResolvers;

    private final ObjectResolver defaultResolver;

    /**
     * @param staticResolvers {@link TypeMatchLookup}
     * @param defaultResolver {@link ObjectResolver}
     */
    public InjectedAnnotationVisitor(TypeMatchLookup<ObjectResolver> staticResolvers, ObjectResolver defaultResolver) {
        super();
        this.defaultResolver = defaultResolver;
        this.staticResolvers = staticResolvers;
    }

    /**
     * {@inheritDoc}
     * 
     * @see jmine.tec.utils.annotation.visitor.AnnotationVisitorAdapter#onParameterAnnotation(java.lang.annotation.Annotation,
     * java.lang.Class, int, java.lang.Class, java.lang.reflect.Type, java.lang.reflect.Member)
     */
    @Override
    protected Collection<? extends DependencyMetaData> onParameterAnnotation(Annotation annotation, Class<?> annotated, int parameterIndex,
            Class<?> parameterType, Type genericType, Member annotatedMember) {
        return this.createDependencyMetadata(annotation, annotated, parameterType, genericType, annotatedMember);
    }

    /**
     * Cria e devolve os {@link DependencyMetaData}
     * 
     * @param annotation a annotation
     * @param annotated a classe anotada
     * @param parameterType o tipo do parametro 'raw'
     * @param genericType o tipo generico (se algum)
     * @param classMember o Member que contem a annotation
     * @return {@link DependencyMetaData}
     */
    private Collection<? extends DependencyMetaData> createDependencyMetadata(Annotation annotation, Class<?> annotated,
            Class<?> parameterType, Type genericType, Member classMember) {
        Class<?> valueType = resolveGenericType(genericType, annotated);
        ObjectResolver objectResolver = this.staticResolvers.lookup(valueType);
        if (objectResolver == null) {
            objectResolver = this.staticResolvers.lookup(parameterType);
            if (objectResolver == null) {
                if (this.defaultResolver.isTypeCompatible(parameterType) || this.defaultResolver.isTypeCompatible(valueType)) {
                    objectResolver = this.defaultResolver;
                } else {
                    throw new IllegalStateException("Cannot inject type: " + parameterType);
                }
            }
        }
        Injected in = (Injected) annotation;
        Wrapper wrapper = Wrappers.findWrapperFor(parameterType);
        return Collections.singleton(new DependencyMetaDataImpl("<auto>", objectResolver, in.required(), wrapper, valueType));
    }

    /**
     * {@inheritDoc}
     * 
     * @see jmine.tec.utils.annotation.visitor.AnnotationVisitorAdapter#onMethodAnnotation(java.lang.annotation.Annotation, java.lang.Class,
     * java.lang.reflect.Method)
     */
    @Override
    public Collection<? extends DependencyMetaData> onMethodAnnotation(Annotation ann, Class<?> declared, Method method) {
        Type type = method.getGenericParameterTypes()[0];
        Class<?> paramType = method.getParameterTypes()[0];
        return this.createDependencyMetadata(ann, declared, paramType, type, method);
    }

    /**
     * Resolve o tipo de um {@link Type}. Se for um {@link TypeVariable}, tenta resolver de acordo com a subclasse. Se for um
     * {@link ParameterizedType}, resolve o uiltimo tipo parametrico declarado e o primeiro tipo parametrico deve ser String.
     * 
     * @param typeToResolve o {@link Type}
     * @param declaringClass o {@link Class} que declara
     * @return {@link Class}
     */
    public static final Class<?> resolveGenericType(Type typeToResolve, Class<?> declaringClass) {
        if (typeToResolve instanceof Class<?>) {
            return (Class<?>) typeToResolve;
        }
        if (typeToResolve instanceof TypeVariable<?>) {
            return GenericTypeResolverImpl.getInstance().resolveTypeForFixedGenericType((TypeVariable<?>) typeToResolve, declaringClass);
        }
        if (typeToResolve instanceof ParameterizedType) {
            ParameterizedType paramType = (ParameterizedType) typeToResolve;
            int index = paramType.getActualTypeArguments().length - 1;
            return GenericTypeResolverImpl.getInstance().resolveTypeForParameterizedType(paramType, declaringClass, index);
        }
        if (typeToResolve instanceof GenericArrayType) {
            return GenericTypeResolverImpl.getInstance().resolveComponentTypeForFixedGenericArrayType((GenericArrayType) typeToResolve,
                    declaringClass);
        }

        throw new IllegalArgumentException("unresolvable type");
    }

    /**
     * {@inheritDoc}
     * 
     * @see jmine.tec.utils.annotation.visitor.AnnotationVisitorAdapter#acceptsMethod(java.lang.annotation.Annotation,
     * java.lang.reflect.Method, java.lang.Class)
     */
    @Override
    public boolean acceptsMethod(Annotation ann, Method method, Class<?> declaring) {
        return ann instanceof Injected && method.getParameterTypes().length == 1;
    }

    /**
     * {@inheritDoc}
     * 
     * @see jmine.tec.utils.annotation.visitor.AnnotationVisitorAdapter#acceptsConstructorParameter(java.lang.annotation.Annotation,
     * java.lang.Class, java.lang.reflect.Constructor, int, java.lang.Class, java.lang.reflect.Type)
     */
    @Override
    public boolean acceptsConstructorParameter(Annotation annotation, Class<?> declaring, Constructor<?> ctor, int parameterIndex,
            Class<?> paramterType, Type genericType) {
        return annotation instanceof Injected;
    }

    /**
     * {@inheritDoc}
     * 
     * @see jmine.tec.utils.annotation.visitor.AnnotationVisitorAdapter#acceptsMethodParameter(java.lang.annotation.Annotation,
     * java.lang.Class, java.lang.reflect.Method, int, java.lang.Class, java.lang.reflect.Type)
     */
    @Override
    public boolean acceptsMethodParameter(Annotation annotation, Class<?> declaring, Method method, int parameterIndex,
            Class<?> paramterType, Type genericType) {
        return annotation instanceof Injected;
    }

}
